/* 
Copyright Paul James Mutton, 2001-2007, http://www.jibble.org/

This file is part of PircBot.

This software is dual-licensed, allowing you to choose between the GNU
General Public License (GPL) and the www.jibble.org Commercial License.
Since the GPL may be too restrictive for use in a proprietary application,
a commercial license is also provided. Full license information can be
found at http://www.jibble.org/licenses/

 */

package org.jibble.pircbot;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * This class is used to administer a DCC file transfer.
 * 
 * @since 1.2.0
 * @author Paul James Mutton, <a href="http://www.jibble.org/">http://www.jibble.org/</a>
 * @version 1.4.6 (Build time: Wed Apr 11 19:20:59 2007)
 */
public class DccFileTransfer {

   /**
    * The default buffer size to use when sending and receiving files.
    */
   public static final int BUFFER_SIZE = 1024;

   private final PircBot _bot;

   private final DccManager _manager;

   private final String _nick;

   private String _login = null;

   private String _hostname = null;

   private String _type;

   private long _address;

   private int _port;

   private final long _size;

   private boolean _received;

   private Socket _socket = null;

   private long _progress = 0;

   private File _file = null;

   private int _timeout = 0;

   private final boolean _incoming;

   private long _packetDelay = 0;

   private long _startTime = 0;

   /**
    * Constructor used for sending files.
    */
   DccFileTransfer(final PircBot bot, final DccManager manager, final File file, final String nick, final int timeout) {
	  _bot = bot;
	  _manager = manager;
	  _nick = nick;
	  _file = file;
	  _size = file.length();
	  _timeout = timeout;
	  _received = true;

	  _incoming = false;
   }

   /**
    * Constructor used for receiving files.
    */
   DccFileTransfer(final PircBot bot, final DccManager manager, final String nick, final String login, final String hostname,
		 final String type, final String filename, final long address, final int port, final long size) {
	  _bot = bot;
	  _manager = manager;
	  _nick = nick;
	  _login = login;
	  _hostname = hostname;
	  _type = type;
	  _file = new File(filename);
	  _address = address;
	  _port = port;
	  _size = size;
	  _received = false;

	  _incoming = true;
   }

   /**
    * Stops the DCC file transfer by closing the connection.
    */
   public void close() {
	  try {
		 _socket.close();
	  } catch (Exception e) {
		 // Let the DCC manager worry about anything that may go wrong.
	  }
   }

   /**
    * Delay between packets.
    */
   private void delay() {
	  if (_packetDelay > 0) {
		 try {
			Thread.sleep(_packetDelay);
		 } catch (InterruptedException e) {
			// Do nothing.
		 }
	  }
   }

   /**
    * Receive the file in a new thread.
    */
   void doReceive(final File file, final boolean resume) {
	  new Thread() {

		 @Override
		 public void run() {

			BufferedOutputStream foutput = null;
			Exception exception = null;

			try {

			   // Convert the integer address to a proper IP address.
			   int[] ip = _bot.longToIp(_address);
			   String ipStr = ip[0] + "." + ip[1] + "." + ip[2] + "." + ip[3];

			   // Connect the socket and set a timeout.
			   _socket = new Socket(ipStr, _port);
			   _socket.setSoTimeout(30 * 1000);
			   _startTime = System.currentTimeMillis();

			   // No longer possible to resume this transfer once it's
			   // underway.
			   _manager.removeAwaitingResume(DccFileTransfer.this);

			   BufferedInputStream input = new BufferedInputStream(_socket.getInputStream());
			   BufferedOutputStream output = new BufferedOutputStream(_socket.getOutputStream());

			   // Following line fixed for jdk 1.1 compatibility.
			   foutput = new BufferedOutputStream(new FileOutputStream(file.getCanonicalPath(), resume));

			   byte[] inBuffer = new byte[BUFFER_SIZE];
			   byte[] outBuffer = new byte[4];
			   int bytesRead = 0;
			   while ((bytesRead = input.read(inBuffer, 0, inBuffer.length)) != -1) {
				  foutput.write(inBuffer, 0, bytesRead);
				  _progress += bytesRead;
				  // Send back an acknowledgement of how many bytes we
				  // have got so far.
				  outBuffer[0] = (byte) (_progress >> 24 & 0xff);
				  outBuffer[1] = (byte) (_progress >> 16 & 0xff);
				  outBuffer[2] = (byte) (_progress >> 8 & 0xff);
				  outBuffer[3] = (byte) (_progress >> 0 & 0xff);
				  output.write(outBuffer);
				  output.flush();
				  delay();
			   }
			   foutput.flush();
			} catch (Exception e) {
			   exception = e;
			} finally {
			   try {
				  foutput.close();
				  _socket.close();
			   } catch (Exception anye) {
				  // Do nothing.
			   }
			}

			_bot.onFileTransferFinished(DccFileTransfer.this, exception);
		 }
	  }.start();
   }

   /**
    * Method to send the file inside a new thread.
    */
   protected void doSend(final boolean allowResume) {
	  new Thread() {

		 @Override
		 public void run() {

			BufferedInputStream finput = null;
			Exception exception = null;

			try {

			   ServerSocket ss = null;

			   int[] ports = _bot.getDccPorts();
			   if (ports == null) {
				  // Use any free port.
				  ss = new ServerSocket(0);
			   } else {
				  for (int port : ports) {
					 try {
						ss = new ServerSocket(port);
						// Found a port number we could use.
						break;
					 } catch (Exception e) {
						// Do nothing; go round and try another port.
					 }
				  }
				  if (ss == null)
					 // No ports could be used.
					 throw new IOException("All ports returned by getDccPorts() are in use.");
			   }

			   ss.setSoTimeout(_timeout);
			   _port = ss.getLocalPort();
			   InetAddress inetAddress = _bot.getDccInetAddress();
			   if (inetAddress == null) {
				  inetAddress = _bot.getInetAddress();
			   }
			   byte[] ip = inetAddress.getAddress();
			   long ipNum = _bot.ipToLong(ip);

			   // Rename the filename so it has no whitespace in it when we
			   // send it.
			   // .... I really should do this a bit more nicely at some
			   // point ....
			   String safeFilename = _file.getName().replace(' ', '_');
			   safeFilename = safeFilename.replace('\t', '_');

			   if (allowResume) {
				  _manager.addAwaitingResume(DccFileTransfer.this);
			   }

			   // Send the message to the user, telling them where to
			   // connect to in order to get the file.
			   _bot.sendCTCPCommand(_nick, "DCC SEND " + safeFilename + " " + ipNum + " " + _port + " " + _file.length());

			   // The client may now connect to us and download the file.
			   _socket = ss.accept();
			   _socket.setSoTimeout(30000);
			   _startTime = System.currentTimeMillis();

			   // No longer possible to resume this transfer once it's
			   // underway.
			   if (allowResume) {
				  _manager.removeAwaitingResume(DccFileTransfer.this);
			   }

			   // Might as well close the server socket now; it's finished
			   // with.
			   ss.close();

			   BufferedOutputStream output = new BufferedOutputStream(_socket.getOutputStream());
			   BufferedInputStream input = new BufferedInputStream(_socket.getInputStream());
			   finput = new BufferedInputStream(new FileInputStream(_file));

			   // Check for resuming.
			   if (_progress > 0) {
				  long bytesSkipped = 0;
				  while (bytesSkipped < _progress) {
					 bytesSkipped += finput.skip(_progress - bytesSkipped);
				  }
			   }

			   byte[] outBuffer = new byte[BUFFER_SIZE];
			   byte[] inBuffer = new byte[4];
			   int bytesRead = 0;
			   while ((bytesRead = finput.read(outBuffer, 0, outBuffer.length)) != -1) {
				  output.write(outBuffer, 0, bytesRead);
				  output.flush();
				  input.read(inBuffer, 0, inBuffer.length);
				  _progress += bytesRead;
				  delay();
			   }
			} catch (Exception e) {
			   exception = e;
			} finally {
			   try {
				  finput.close();
				  _socket.close();
			   } catch (Exception e) {
				  // Do nothing.
			   }
			}

			_bot.onFileTransferFinished(DccFileTransfer.this, exception);
		 }
	  }.start();
   }

   /**
    * Returns the suggested file to be used for this transfer.
    * 
    * @return the suggested file to be used.
    * 
    */
   public File getFile() {
	  return _file;
   }

   /**
    * Returns the hostname of the file sender.
    * 
    * @return the hostname of the file sender. null if we are sending.
    * 
    */
   public String getHostname() {
	  return _hostname;
   }

   /**
    * Returns the login of the file sender.
    * 
    * @return the login of the file sender. null if we are sending.
    * 
    */
   public String getLogin() {
	  return _login;
   }

   /**
    * Returns the nick of the other user taking part in this file transfer.
    * 
    * @return the nick of the other user.
    * 
    */
   public String getNick() {
	  return _nick;
   }

   /**
    * Returns the address of the sender as a long.
    * 
    * @return the address of the sender as a long.
    */
   public long getNumericalAddress() {
	  return _address;
   }

   /**
    * returns the delay time between each packet that is send or received.
    * 
    * @return the delay between each packet.
    * 
    */
   public long getPacketDelay() {
	  return _packetDelay;
   }

   /**
    * Returns the port number to be used when making the connection.
    * 
    * @return the port number.
    * 
    */
   public int getPort() {
	  return _port;
   }

   /**
    * Returns the progress (in bytes) of the current file transfer. When resuming, this represents the total number of bytes in
    * the file, which may be greater than the amount of bytes resumed in just this transfer.
    * 
    * @return the progress of the transfer.
    */
   public long getProgress() {
	  return _progress;
   }

   /**
    * Returns the progress of the file transfer as a percentage. Note that this should never be negative, but could become greater
    * than 100% if you attempt to resume a larger file onto a partially downloaded file that was smaller.
    * 
    * @return the progress of the transfer as a percentage.
    */
   public double getProgressPercentage() {
	  return 100 * getProgress() / (double) getSize();
   }

   /**
    * Returns the size (in bytes) of the file being transfered.
    * 
    * @return the size of the file. Returns -1 if the sender did not specify this value.
    */
   public long getSize() {
	  return _size;
   }

   /**
    * Returns the rate of data transfer in bytes per second. This value is an estimate based on the number of bytes transfered
    * since the connection was established.
    * 
    * @return data transfer rate in bytes per second.
    */
   public long getTransferRate() {
	  long time = (System.currentTimeMillis() - _startTime) / 1000;
	  if (time <= 0)
		 return 0;
	  return getProgress() / time;
   }

   /**
    * Returns true if the file transfer is incoming (somebody is sending the file to us).
    * 
    * @return true if the file transfer is incoming.
    * 
    */
   public boolean isIncoming() {
	  return _incoming;
   }

   /**
    * Returns true if the file transfer is outgoing (we are sending the file to someone).
    * 
    * @return true if the file transfer is outgoing.
    * 
    */
   public boolean isOutgoing() {
	  return !isIncoming();
   }

   /**
    * Receives a DccFileTransfer and writes it to the specified file. Resuming allows a partial download to be continue from the
    * end of the current file contents.
    * 
    * @param file
    *           The file to write to.
    * @param resume
    *           True if you wish to try and resume the download instead of overwriting an existing file.
    * 
    */
   public void receive(final File file, final boolean resume) {
	  if (!_received) {
		 _received = true;
		 _file = file;

		 if ("SEND".equals(_type) && resume) {
			_progress = file.length();
			if (_progress == 0) {
			   doReceive(file, false);
			} else {
			   _bot.sendCTCPCommand(_nick, "DCC RESUME file.ext " + _port + " " + _progress);
			   _manager.addAwaitingResume(this);
			}
		 } else {
			_progress = file.length();
			doReceive(file, resume);
		 }
	  }
   }

   /**
    * Sets the delay time between sending or receiving each packet. Default is 0. This is useful for throttling the speed of file
    * transfers to maintain a good quality of service for other things on the machine or network.
    * 
    * @param millis
    *           The number of milliseconds to wait between packets.
    * 
    */
   public void setPacketDelay(final long millis) {
	  _packetDelay = millis;
   }

   /**
    * Package mutator for setting the progress of the file transfer.
    */
   protected void setProgress(final long progress) {
	  _progress = progress;
   }

}
